﻿namespace ClassyBlog.Infrastructure
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Web;
    using System.Web.Hosting;
    using System.Web.Mvc;
    using System.Web.Routing;

    using Web;

    public abstract class ThemeableVirtualPathProviderViewEngine : IViewEngine
    {
        private const string CacheKeyFormat =
            ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}:";

        private const string CacheKeyPrefixMaster = "Master";
        private const string CacheKeyPrefixPartial = "Partial";
        private const string CacheKeyPrefixView = "View";

        private static readonly string[] emptyLocations = new string[0];

        private VirtualPathProvider virtualPathProvider;

        protected ThemeableVirtualPathProviderViewEngine()
        {
            ViewLocationCache = HttpContext.Current == null ||
                                HttpContext.Current.IsDebuggingEnabled ?
                                DefaultViewLocationCache.Null :
                                new DefaultViewLocationCache();

            AreaMasterLocationFormats = new List<string>();
            AreaPartialViewLocationFormats = new List<string>();
            AreaViewLocationFormats = new List<string>();
            MasterLocationFormats = new List<string>();
            PartialViewLocationFormats = new List<string>();
            ViewLocationFormats = new List<string>();
        }

        public Func<string> CurrentTheme { get; set; }

        public ICollection<string> AreaMasterLocationFormats
        {
            get; private set;
        }

        public ICollection<string> AreaPartialViewLocationFormats
        {
            get; private set;
        }

        public ICollection<string> AreaViewLocationFormats
        {
            get; private set;
        }

        public ICollection<string> MasterLocationFormats { get; private set; }

        public ICollection<string> PartialViewLocationFormats
        {
            get; private set;
        }

        public ICollection<string> ViewLocationFormats { get; private set; }

        protected IViewLocationCache ViewLocationCache { get; set; }

        protected VirtualPathProvider VirtualPathProvider
        {
            get
            {
                return virtualPathProvider ??
                       (virtualPathProvider =
                       HostingEnvironment.VirtualPathProvider);
            }

            set
            {
                virtualPathProvider = value;
            }
        }

        public virtual ViewEngineResult FindView(
            ControllerContext controllerContext,
            string viewName,
            string masterName,
            bool useCache)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }

            if (string.IsNullOrWhiteSpace(viewName))
            {
                throw new ArgumentException(
                   TextMessages.
                   ThemeableVirtualPathProviderViewEngine_Find_View_Name_Blank,
                   "viewName");
            }

            string[] viewLocationsSearched;
            string[] masterLocationsSearched;

            var incompleteMatch = false;

            var controllerName = controllerContext.RouteData
                .GetRequiredString("controller");

            var viewPath = GetPath(
                controllerContext,
                ViewLocationFormats,
                AreaViewLocationFormats,
                "ViewLocationFormats",
                viewName,
                controllerName,
                CacheKeyPrefixView,
                useCache,
                /* checkPathValidity */ true,
                ref incompleteMatch,
                out viewLocationsSearched);

            var masterPath = GetPath(
                controllerContext,
                MasterLocationFormats,
                AreaMasterLocationFormats,
                "MasterLocationFormats",
                masterName,
                controllerName,
                CacheKeyPrefixMaster,
                useCache,
                /* checkPathValidity */ false,
                ref incompleteMatch,
                out masterLocationsSearched);

            if (string.IsNullOrWhiteSpace(viewPath) ||
                (string.IsNullOrWhiteSpace(masterPath) &&
                !string.IsNullOrWhiteSpace(masterName)))
            {
                return new ViewEngineResult(
                    viewLocationsSearched.Union(masterLocationsSearched));
            }

            return new ViewEngineResult(
                CreateView(controllerContext, viewPath, masterPath), this);
        }

        public virtual ViewEngineResult FindPartialView(
            ControllerContext controllerContext,
            string partialViewName,
            bool useCache)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }

            if (string.IsNullOrWhiteSpace(partialViewName))
            {
                throw new ArgumentException(
                   TextMessages.
                   ThemeableVirtualPathProviderViewEngine_Find_View_Name_Blank,
                   "partialViewName");
            }

            string[] searched;

            var incompleteMatch = false;
            var controllerName = controllerContext.RouteData
                .GetRequiredString("controller");

            var partialPath = GetPath(
                controllerContext,
                PartialViewLocationFormats,
                AreaPartialViewLocationFormats,
                "PartialViewLocationFormats",
                partialViewName,
                controllerName,
                CacheKeyPrefixPartial,
                useCache,
                /* checkBaseType */ true,
                ref incompleteMatch,
                out searched);

            return string.IsNullOrWhiteSpace(partialPath) ?
                   new ViewEngineResult(searched) :
                   new ViewEngineResult(
                       CreatePartialView(
                       controllerContext, partialPath),
                       this);
        }

        public virtual void ReleaseView(
            ControllerContext controllerContext, IView view)
        {
            var disposable = view as IDisposable;

            if (disposable != null)
            {
                disposable.Dispose();
            }
        }

        protected virtual bool FileExists(
            ControllerContext controllerContext, string virtualPath)
        {
            return VirtualPathProvider.FileExists(virtualPath);
        }

        protected virtual bool? IsValidPath(
            ControllerContext controllerContext, string virtualPath)
        {
            return null;
        }

        protected abstract IView CreatePartialView(
            ControllerContext controllerContext, string partialPath);

        protected abstract IView CreateView(
            ControllerContext controllerContext,
            string viewPath,
            string masterPath);

        private static IList<ViewLocation> GetViewLocations(
            IEnumerable<string> viewLocationFormats,
            IEnumerable<string> areaViewLocationFormats)
        {
            var allLocations = new List<ViewLocation>();

            if (areaViewLocationFormats != null)
            {
                allLocations.AddRange(
                    areaViewLocationFormats.Select(areaViewLocationFormat =>
                        new AreaAwareViewLocation(areaViewLocationFormat)));
            }

            if (viewLocationFormats != null)
            {
                allLocations.AddRange(viewLocationFormats.Select(
                    viewLocationFormat =>
                        new ViewLocation(viewLocationFormat)));
            }

            return allLocations;
        }

        private static bool IsSpecificPath(string name)
        {
            var c = name.First();

            return c == '~' || c == '/';
        }

        private static string GetAreaName(RouteData routeData)
        {
            object area;

            return routeData.DataTokens.TryGetValue("area", out area) ?
                   area as string :
                   GetAreaName(routeData.Route);
        }

        private static string GetAreaName(RouteBase route)
        {
            var areaRoute = route as IRouteWithArea;

            if (areaRoute != null)
            {
                return areaRoute.Area;
            }

            var concreteRoute = route as Route;

            return (concreteRoute != null) &&
                   (concreteRoute.DataTokens != null) ?
                   concreteRoute.DataTokens["area"] as string :
                   null;
        }

        private string CreateCacheKey(string theme, string prefix, string name, string controllerName, string areaName)
        {
            return string.Format(
                CultureInfo.InvariantCulture,
                CacheKeyFormat,
                GetType().AssemblyQualifiedName,
                theme,
                prefix,
                name,
                controllerName,
                areaName);
        }

        private string GetPath(
            ControllerContext controllerContext,
            IEnumerable<string> locations,
            IEnumerable<string> areaLocations,
            string locationsPropertyName,
            string name,
            string controllerName,
            string cacheKeyPrefix,
            bool useCache,
            bool checkPathValidity,
            ref bool incompleteMatch,
            out string[] searchedLocations)
        {
            searchedLocations = emptyLocations;

            if (string.IsNullOrWhiteSpace(name))
            {
                return string.Empty;
            }

            var areaName = GetAreaName(controllerContext.RouteData);
            var usingAreas = !string.IsNullOrEmpty(areaName);
            var theme = CurrentTheme();

            var viewLocations = GetViewLocations(
                locations, usingAreas ? areaLocations : null);

            if (!viewLocations.Any())
            {
                throw new InvalidOperationException(
                string.Format(
                CultureInfo.CurrentCulture,
                TextMessages.
                ThemeableVirtualPathProviderViewEngine_GetPath_NoViewLocations,
                locationsPropertyName));
            }

            var nameRepresentsPath = IsSpecificPath(name);

            var cacheKey = CreateCacheKey(
                theme,
                cacheKeyPrefix,
                name,
                nameRepresentsPath ? string.Empty : controllerName,
                areaName);

            if (useCache)
            {
                return ViewLocationCache.GetViewLocation(
                    controllerContext.HttpContext, cacheKey);
            }

            return nameRepresentsPath ?
                   GetPathFromSpecificName(
                   controllerContext,
                   name,
                   cacheKey,
                   checkPathValidity,
                   ref searchedLocations,
                   ref incompleteMatch) :
                   GetPathFromGeneralName(
                   controllerContext,
                   viewLocations,
                   name,
                   controllerName,
                   areaName,
                   theme,
                   cacheKey,
                   out searchedLocations);
        }

        private string GetPathFromGeneralName(
            ControllerContext controllerContext,
            IList<ViewLocation> locations,
            string name,
            string controllerName,
            string areaName,
            string theme,
            string cacheKey,
            out string[] searchedLocations)
        {
            var result = string.Empty;
            searchedLocations = new string[locations.Count];

            for (var i = 0; i < locations.Count; i++)
            {
                var location = locations[i];
                var virtualPath = location.Format(
                    name, controllerName, areaName, theme);

                if (FileExists(controllerContext, virtualPath))
                {
                    searchedLocations = emptyLocations;
                    result = virtualPath;
                    ViewLocationCache.InsertViewLocation(
                        controllerContext.HttpContext, cacheKey, result);
                    break;
                }

                searchedLocations[i] = virtualPath;
            }

            return result;
        }

        private string GetPathFromSpecificName(
            ControllerContext controllerContext,
            string name,
            string cacheKey,
            bool checkPathValidity,
            ref string[] searchedLocations,
            ref bool incompleteMatch)
        {
            var result = name;
            var fileExists = FileExists(controllerContext, name);

            if (checkPathValidity && fileExists)
            {
                var validPath = IsValidPath(controllerContext, name);

                switch (validPath)
                {
                    case false:
                        fileExists = false;
                        break;
                    case null:
                        incompleteMatch = true;
                        break;
                }
            }

            if (!fileExists)
            {
                result = string.Empty;
                searchedLocations = new[] { name };
            }

            if (!incompleteMatch)
            {
                ViewLocationCache.InsertViewLocation(
                    controllerContext.HttpContext, cacheKey, result);
            }

            return result;
        }

        private class ViewLocation
        {
            protected readonly string VirtualPathFormatString;

            public ViewLocation(string virtualPathFormatString)
            {
                VirtualPathFormatString = virtualPathFormatString;
            }

            public virtual string Format(
                string viewName,
                string controllerName,
                string areaName,
                string theme)
            {
                return string.Format(
                    CultureInfo.InvariantCulture,
                    VirtualPathFormatString,
                    viewName,
                    controllerName,
                    theme);
            }
        }

        private class AreaAwareViewLocation : ViewLocation
        {
            public AreaAwareViewLocation(string virtualPathFormatString) :
                base(virtualPathFormatString)
            {
            }

            public override string Format(
                string viewName,
                string controllerName,
                string areaName,
                string theme)
            {
                return string.Format(
                    CultureInfo.InvariantCulture,
                    VirtualPathFormatString,
                    viewName,
                    controllerName,
                    areaName,
                    theme);
            }
        }
    }
}